#include "engine.h"
#include "internal_types.h"
#include <string>
#include <fstream>
#include <set>
using namespace std;

namespace HW
{

using namespace Resource;

static set<TShader> lShaders;
static TShader gCurrentShader;
void _i_ReadShader ( string& out , const_str add );
void _i_ResolveInclude ( string& in );
int _i_GetUniAdd ( TShader shader , string name );
int _i_GetAttribAdd ( TShader shader , string name );

TShader LoadShader(const_str vertpath, const_str fragpath, const_str defines)
{
	string lVertPath = AbstractPath(vertpath);
	string lFragPath = AbstractPath(fragpath);
	AppLog ( string("Loading shader: \"" + lVertPath + "\" , \"" + lFragPath + "\"").c_str() );

	extern bool _i_FileExist ( const_str path );
	if ( ! ( _i_FileExist ( lVertPath.c_str() ) || _i_FileExist ( lFragPath.c_str() ) ) )
	{
		AppLog ( string("Failed to load shader: \"" + lVertPath + "\" , \"" + lFragPath + "\"").c_str() );
		return NULL;
	}

	_i_Shader* lNewShader = new _i_Shader();

	GLuint lvertShader = glCreateShader ( GL_VERTEX_SHADER );
	GLuint lfragShader = glCreateShader ( GL_FRAGMENT_SHADER );

	static string lShaderStr;

	_i_ReadShader ( lShaderStr , AbstractPath(vertpath) );
	const GLchar* vtmp = static_cast<const char*>(lShaderStr.c_str());
	glShaderSource ( lvertShader , 1 , &vtmp , NULL );
	glCompileShader ( lvertShader );

	_i_ReadShader ( lShaderStr , AbstractPath(fragpath) );
	const GLchar* ftmp = static_cast<const char*>(lShaderStr.c_str());
	glShaderSource ( lfragShader , 1 , &ftmp , NULL );
	glCompileShader ( lfragShader );


	char lShaderErrorStr[1000];
	int lShaderErrorLen;
	GLint result;
	glGetShaderiv(lvertShader, GL_COMPILE_STATUS, &result);
	if (result == GL_FALSE)
	{
		glGetShaderInfoLog ( lvertShader , 1000 , &lShaderErrorLen , lShaderErrorStr );
		if ( lShaderErrorLen+1 < 1000 )
			lShaderErrorStr[lShaderErrorLen+1] = NULL;
		AppLog ( lShaderErrorStr , APPLOG_WARNING );
		glDeleteShader ( lvertShader );
		glDeleteShader ( lfragShader );
		delete lNewShader;
		return NULL;
	}
	glGetShaderiv(lfragShader, GL_COMPILE_STATUS, &result);
	if (result == GL_FALSE)
	{
		glGetShaderInfoLog ( lfragShader , 1000 , &lShaderErrorLen , lShaderErrorStr );
		if ( lShaderErrorLen+1 < 1000 )
			lShaderErrorStr[lShaderErrorLen+1] = NULL;
		AppLog ( lShaderErrorStr , APPLOG_WARNING );
		glDeleteShader ( lvertShader );
		glDeleteShader ( lfragShader );
		delete lNewShader;
		return NULL;
	}

	lNewShader -> id = glCreateProgram ( );
	glAttachShader ( lNewShader->id , lvertShader );
	glAttachShader ( lNewShader->id , lfragShader );

	glLinkProgram ( lNewShader->id );
	glDeleteShader ( lvertShader );
	glDeleteShader ( lfragShader );

	glGetProgramiv(lNewShader->id, GL_LINK_STATUS, &result);
	if (result == GL_FALSE)
	{
		glGetProgramInfoLog ( lNewShader->id , 1000 , &lShaderErrorLen , lShaderErrorStr );
		if ( lShaderErrorLen+1 < 1000 )
			lShaderErrorStr[lShaderErrorLen+1] = NULL;
		AppLog ( lShaderErrorStr , APPLOG_WARNING );
		glDeleteProgram ( lNewShader->id );
		delete lNewShader;
		return NULL;
	}

	lShaders.insert ( (TShader)(lNewShader) );
	return (TShader)(lNewShader);
}

void SetShader(TShader shader)
{
	if ( shader == NULL )
		glUseProgram ( 0 );
	else
		glUseProgram ( ((_i_Shader*)(shader))->id );
	gCurrentShader = shader;
}

void SetShaderInt(TShader shader, const_str name, int value)
{
	TShader lTmpCurrShader = gCurrentShader;

	SetShader ( shader );
	glUniform1i ( _i_GetAttribAdd(shader,name) , value );

	SetShader ( lTmpCurrShader );
}

void SetShaderIntArray(TShader shader, const_str name, int* ar, int count)
{
	TShader lTmpCurrShader = gCurrentShader;

	SetShader ( shader );
	glUniform1iv ( _i_GetAttribAdd(shader,name) , count , ar );

	SetShader ( lTmpCurrShader );
}

void SetShaderFloat(TShader shader, const_str name, flt value)
{
	TShader lTmpCurrShader = gCurrentShader;

	SetShader ( shader );
	glUniform1f ( _i_GetAttribAdd(shader,name) , value );

	SetShader ( lTmpCurrShader );
}

void SetShaderFloatArray(TShader shader, const_str name, flt* ar, int count)
{
	TShader lTmpCurrShader = gCurrentShader;

	SetShader ( shader );
	glUniform1fv ( _i_GetAttribAdd(shader,name) , count , ar );

	SetShader ( lTmpCurrShader );
}

void SetShaderVec2(TShader shader, const_str name, const TVec2 &value, int count)
{
	TShader lTmpCurrShader = gCurrentShader;

	SetShader ( shader );
	glUniform2fv ( _i_GetAttribAdd(shader,name) , 1 , value.a );

	SetShader ( lTmpCurrShader );
}

void SetShaderVec3(TShader shader, const_str name, const TVec3 &value, int count)
{
	TShader lTmpCurrShader = gCurrentShader;

	SetShader ( shader );
	glUniform3fv ( _i_GetAttribAdd(shader,name) , 1 , value.a );

	SetShader ( lTmpCurrShader );
}

void SetShaderVec4(TShader shader, const_str name, const TVec4 &value, int count)
{
	TShader lTmpCurrShader = gCurrentShader;

	SetShader ( shader );
	glUniform4fv ( _i_GetAttribAdd(shader,name) , 1 , value.a );

	SetShader ( lTmpCurrShader );
}

void FreeShader(TShader shader)
{
	AppLog ( "Freeing shader." );
	if ( gCurrentShader == shader )
		SetShader ( NULL );
	glDeleteProgram ( ((_i_Shader*)(shader))->id );
	lShaders.erase ( shader );
	delete shader;
}

void SetShaderMat3(TShader shader, const_str name, const TVec9 &value, int count)
{
	TShader lTmpCurrShader = gCurrentShader;

	SetShader ( shader );
	glUniformMatrix3fv ( _i_GetAttribAdd(shader,name) , 1 , GL_TRUE , value.a );

	SetShader ( lTmpCurrShader );
}

void SetShaderMat4(TShader shader, const_str name, const TVec16 &value, int count)
{
	TShader lTmpCurrShader = gCurrentShader;

	SetShader ( shader );
	glUniformMatrix4fv ( _i_GetAttribAdd(shader,name) , 1 , GL_TRUE , value.a );

	SetShader ( lTmpCurrShader );
}

void _i_ReadShader ( string& out , const_str add )
{
	out = "";

	ifstream shaderFile ( add , ios::in );
	if ( !shaderFile.is_open ( ) )
		return;

	streamoff length;
	char * buffer;
	
	// get length of file:
	shaderFile.seekg (0, ios::end);
	length = shaderFile.tellg();
	shaderFile.seekg (0, ios::beg);

	// allocate memory:
	buffer = new char [int(length)];
	memset ( buffer , NULL , size_t(length) * sizeof(char) );
	// read data as a block:
	shaderFile.read (buffer,length);
	out.append (buffer);

	shaderFile.close ( );
	delete buffer;

	_i_ResolveInclude ( out );
}

void _i_ResolveInclude ( string& in )
{
	while ( in.find ( "#include" , 0 ) != string::npos )
	{
		size_t beginL = in.find ( "#include" , 0 );
		size_t endL = in.find ( "\n" , beginL );

		size_t beginA = in.find ( "\"" , beginL );
		size_t endA = in.find ( "\"" , beginA+1 );

		if ( beginA < endL &&
			beginA < endA &&
			endA < endL )
		{
			string incAdd = in.substr ( beginA+1 , endA-beginA-1 );

			in.erase ( beginL , endL-beginL );

			string incStr;
			_i_ReadShader ( incStr , incAdd.c_str() );
			in.insert ( beginL , incStr );

		}

		else
			in.erase ( beginL , endL-beginL );
	}
}

void _i_UnInitShaders ( void )
{
	while ( !lShaders.empty() )
		FreeShader ( *lShaders.begin() );
}

int _i_GetUniAdd ( TShader shader , string name )
{
	if ( ((_i_Shader*)(shader))->uniformMap.find(name) == ((_i_Shader*)(shader))->uniformMap.end ( ) )
	{
		GLint add = glGetUniformLocation(((_i_Shader*)(shader))->id, name.c_str());
		((_i_Shader*)(shader))->uniformMap[name] = add;
		if ( add == -1 )
		{
			string lWarn = "Warning: Uniform \"" + name + "\" does not exist in the shader.";
			AppLog ( lWarn.c_str() );
		}
	}
	return ((_i_Shader*)(shader))->uniformMap[name];
}

int _i_GetAttribAdd ( TShader shader , string name )
{
	if ( ((_i_Shader*)(shader))->attribMap.find(name) == ((_i_Shader*)(shader))->attribMap.end ( ) )
	{
		GLint add = glGetAttribLocation ( ((_i_Shader*)(shader))->id , name.c_str() );
		((_i_Shader*)(shader))->attribMap[name] = add;
		if ( add == -1 )
		{
			string lWarn = "Warning: Attribute \"" + name + "\" does not exist in the shader.";
			AppLog ( lWarn.c_str() );
		}
	}
	return ((_i_Shader*)(shader))->uniformMap[name];
}

}